package ar.edu.itba.it.cg.grupo01.game;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Properties;

import ar.edu.itba.it.cg.grupo01.game.action.AccelAction;
import ar.edu.itba.it.cg.grupo01.game.action.DropFlagAction;
import ar.edu.itba.it.cg.grupo01.game.action.HonkAction;
import ar.edu.itba.it.cg.grupo01.game.action.MenuAction;
import ar.edu.itba.it.cg.grupo01.game.action.ResetAction;
import ar.edu.itba.it.cg.grupo01.game.action.ResetGameAction;
import ar.edu.itba.it.cg.grupo01.game.action.SelectCameraAction;
import ar.edu.itba.it.cg.grupo01.game.action.SteerAction;
import ar.edu.itba.it.cg.grupo01.game.action.score.Score;
import ar.edu.itba.it.cg.grupo01.game.action.score.Scores;
import ar.edu.itba.it.cg.grupo01.game.sound.Pioneer;
import ar.edu.itba.it.cg.grupo01.game.sound.PioneerSoundSystem;
import ar.edu.itba.it.cg.grupo01.game.terrain.Circuit;
import ar.edu.itba.it.cg.grupo01.menu.Menu;
import ar.edu.itba.it.cg.grupo01.menu.Stateable;
import ar.edu.itba.it.cg.grupo01.state.EndGameState;
import ar.edu.itba.it.cg.grupo01.state.GameStateType;
import ar.edu.itba.it.cg.grupo01.state.MenuState;
import ar.edu.itba.it.cg.grupo01.state.MyGameState;
import ar.edu.itba.it.cg.grupo01.state.PlayState;
import ar.edu.itba.it.cg.grupo01.utils.Utils;
import ar.edu.itba.it.cg.grupo01.vehicle.Car;
import ar.edu.itba.it.cg.grupo01.vehicle.CarData;
import ar.edu.itba.it.cg.grupo01.vehicle.Hud;

import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.ChaseCamera;
import com.jme.input.InputHandler;
import com.jme.input.KeyInput;
import com.jme.input.action.InputAction;
import com.jme.input.thirdperson.ThirdPersonMouseLook;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.CameraNode;
import com.jme.scene.Node;
import com.jme.scene.Skybox;
import com.jme.scene.Spatial;
import com.jme.scene.state.CullState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;

/**
 * This class tests the advanced vehicle, tuned by Erick B Passos.
 * 
 * @author Erick B Passos
 */
public abstract class BaseGame extends SimplePhysics implements Stateable,
		Observer {

	// the car
	protected Car car;

	// scene stuff
	protected ChaseCamera chaser;
	private CameraNode cameraNode;
	protected Hud hud;
	protected Node hudNode;
	protected Skybox sb;
	protected Circuit circuit;
	protected Properties gameModeProperties;

	// keyed actions
	private InputAction vuvuzelaAction;
	private ResetAction resetAction;
	private InputAction menuAction;
	private DropFlagAction dropFlagAction;
	private SelectCameraAction selectCameraAction;

	// game states
	private MyGameState currentState;
	private MyGameState menuState;
	private MyGameState playState;
	private MyGameState endingState;

	private boolean active;

	private Pioneer audio;

	// game stats variables
	private long currentTime;
	private long gameStart;
	private long gameEnd;
	private long gameDuration;
	private long gamePause;
	private long currentScore;
	private long score;
	private double currentSpeed;
	private long maxScore;
	private long zeroTime;
	private Integer lapAmount;

	private GameMode gameMode = GameMode.LAPS;

	private Score highScore;
	private static Scores scores;

	private boolean gameInProgress;

	private ResetGameAction resetGameAction;

	private AccelAction accelActionUp;

	private AccelAction accelActionDown;

	private SteerAction steerActionUp;

	private SteerAction steerActionDown;

	private String circuitName;

	private Properties generalProperties;

	public BaseGame(String circuitName) {
		super();
		this.circuitName = circuitName;
	}

	@Override
	protected void initSystem() {
		super.initSystem();

		display.setTitle("Rally 100k");
	}

	public ChaseCamera getCamera(){
		return chaser;
	}
	
 	@Override
	protected void simpleUpdate() {

		super.simpleUpdate();

		readInput();

		GameStateType t = currentState.update(timer);

		if (t != currentState.getType()) {
			currentState.leaveState();

			switch (t) {
			case PLAY:
				updateSettings();
				Menu menu = ((MenuState) menuState).getMenu();
				if (menu.isReset())
					resetGame(menu.getGameMode());
				setCurrentState(playState);
				break;
			case MENU:
				setCurrentState(menuState);
				break;
			case GAMEOVER:
				setCurrentState(endingState);
				break;
			}

			currentState.enterState();
			if (!hud.isPlayingCountdown())
				input.setEnabled(this.active);
		}

		// skybox relative to camera
		Vector3f temp = new Vector3f(cam.getLocation());
		sb.setLocalTranslation(temp);

		checkCollissions();

		currentScore += circuit.updateCheckPoint(car, audio, gameMode, currentTime);
		updateGameStats();

	}

	private void updateSettings() {

		Menu menu = ((MenuState) this.menuState).getMenu();
		audio.setMusicVolume(menu.getMusicVolumeValue());
		audio.setSFXVolume(menu.getFxVolumeValue());
	}

	private void updateGameStats() {

		if (active) {
			if (gameInProgress) {
				currentTime = new Date().getTime() - gameStart;
				
				GameEndingState endingState =gameMode.gameEnded(-1, currentTime, gameModeProperties);
				if( endingState != GameEndingState.NOTOVER ) {
					endGame(endingState);
					return;
				}
				
			}
		}

		currentScore = gameMode.getTimeScore(currentTime, maxScore, zeroTime,
				currentScore);
		currentSpeed = car.getLinearSpeed();
		currentSpeed = Math.floor(currentSpeed);

		Integer sp = new Integer((int) currentSpeed);
		hud.setSpeed(sp.toString());

		hud.setTotalLaps(String.valueOf(lapAmount));

		long timeToDisplay = gameMode.getDisplayTime(currentTime,
				gameModeProperties);
		hud.setTime(String.valueOf(timeToDisplay / 1000));

		hud.setPoints(String.valueOf(currentScore));

		String hscore = highScore == null ? "--" : String.valueOf(highScore
				.getScore());
		hud.setHighscore(hscore);

		hud.updateHud();
	}

	private void checkCollissions() {

		if (circuit.collidesWith(car)) {
			audio.playEffect(PioneerSoundSystem.COLLISSION, false);
		}

	}

	/**
	 * This is called every frame in BaseGame.start(), after update()
	 * 
	 * @param interpolation
	 *            unused in this implementation
	 * @see com.jme.app.AbstractGame#render(float interpolation)
	 */
	@Override
	protected final void render(float interpolation) {

		super.render(interpolation);

		if (currentState.getType() == GameStateType.MENU) {
			input.setEnabled(false);
			menuState.render();
		}
	}

	@Override
	protected void simpleInitGame() {
		// Create a ZBuffer to display pixels closest to the camera above
		// farther ones
		// createOptimizations();

		try {
			loadGeneralProperties(circuitName);
			loadGameProperties(circuitName);
		} catch (IOException e) {
			throw new IllegalStateException("Could not initialize properties",
					e);
		} catch (URISyntaxException e) {
			throw new IllegalStateException("Could not initialize properties",
					e);
		}

		// Lets tune our ODEJava first
		tunePhysics();

		// The skybox
		createSky();

		// create HUd
		createHud();

		// Lets put a simple physics floor so our car can race over it
		createCircuit();

		// Lets light up the circuit
		createLights();

		// Lets create our car
		createCar();

		setupCamera();

		// Audio MUST BE CREATED before binding the action keys
		setupAudio();

		createGameStates();

		// Bind keys
		// Key binding MUST BE DONE before updating the scene graph
		initInput(audio);
		// Lets be nice and update our scene graph properties.
		updateSceneGraph();

	}

	public void resetGame(GameMode gameMode) {

		// if (this.gameMode.equals(gameMode)) {
		// resetGameSettings();
		// setCarInitialPosition();
		// circuit.reset();
		// return;
		// }

		this.gameMode = gameMode;
		
		try {
			loadGameProperties(circuitName);
		} catch (IOException e) {
			throw new RuntimeException("Could not reset game", e);
		} catch (URISyntaxException e) {
			throw new RuntimeException("Could not reset game", e);
		}

		circuit.setProperties(gameModeProperties, gameMode, physicsSpace);

		resetGameSettings();

		updateSceneGraph();
		
		hud.setGameEnd(false);
		hud.setGameEndType(GameEndingState.NOTOVER);
		hud.playCountdown(input);

	}

	private void updateSceneGraph() {
		rootNode.updateRenderState();
		resetAction.performAction(null);
	}

	private void createHud() {
		hudNode = new Node();
		rootNode.attachChild(hudNode);
		hudNode.setRenderQueueMode(Renderer.QUEUE_ORTHO);
		hud = new Hud(hudNode, display);
		hud.drawHud();

	}

	private void loadGeneralProperties(String circuitName)
			throws FileNotFoundException, IOException, URISyntaxException {
		generalProperties = new Properties();
		generalProperties.load(new FileInputStream(new File(Utils
				.getResourceURL(
						"properties/" + circuitName + ".general.properties")
				.toURI())));

	}

	private void loadGameProperties(String circuitName) throws IOException,
			URISyntaxException {

		gameModeProperties = new Properties();
		gameModeProperties.load(new FileInputStream(new File(gameMode
				.getGameProperties(circuitName).toURI())));

	}

	private void setupAudio() {

		// TODO: Recieve parameters from music menu
		audio = new Pioneer(true, true);
	}

	private void createOptimizations() {
		ZBufferState buf = display.getRenderer().createZBufferState();
		buf.setEnabled(true);
		buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
		rootNode.setRenderState(buf);

		CullState cs = display.getRenderer().createCullState();
		cs.setCullFace(com.jme.scene.state.CullState.Face.Back);
		cs.setEnabled(true);
		rootNode.setRenderState(cs);

	}

	private ChaseCamera setupCamera() {

		// The target to be followed
		Spatial toBeChased = car.getChassis().getChild(0);
		Vector3f targetOffset = new Vector3f();
		targetOffset.y = ((BoundingBox) toBeChased.getWorldBound()).yExtent * 2.0f;

		// Chase camera props
		HashMap<String, Object> props = new HashMap<String, Object>();
		props.put(ThirdPersonMouseLook.PROP_MAXROLLOUT, "6");
		props.put(ThirdPersonMouseLook.PROP_MINROLLOUT, "3");
		props.put(ChaseCamera.PROP_TARGETOFFSET, targetOffset);
		props.put(ThirdPersonMouseLook.PROP_MAXASCENT, "" + 45
				* FastMath.DEG_TO_RAD);
		props.put(ChaseCamera.PROP_INITIALSPHERECOORDS, new Vector3f(5, 0,
				30 * FastMath.DEG_TO_RAD));
		props.put(ChaseCamera.PROP_TARGETOFFSET, targetOffset);

		// Chase camera
		chaser = new ChaseCamera(cam, toBeChased, props);
		chaser.setMaxDistance(90);
		chaser.setMinDistance(70);
		// chaser.setEnableSpring(false);
		chaser.getMouseLook().setEnabled(false);

		return chaser;

	}

	private void createSky() {
		Texture sky = Utils
				.loadTexture(generalProperties.getProperty("skybox"));
		TextureState ts = display.getRenderer().createTextureState();

		ts.setTexture(sky);

		// BlendState blendState =
		// DisplaySystem.getDisplaySystem().getRenderer()
		// .createBlendState();
		// blendState.setBlendEnabled(true);
		// blendState.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		// blendState
		// .setDestinationFunction(BlendState.DestinationFunction.DestinationAlpha);
		// blendState.setTestEnabled(true);
		// blendState
		// .setTestFunction(BlendState.TestFunction.GreaterThan);
		// blendState.setEnabled(true);

		sb = new Skybox("skybox", 100, 100, 100);
		sb.setTexture(Skybox.Face.North, sky);
		sb.setTexture(Skybox.Face.West, sky);
		sb.setTexture(Skybox.Face.South, sky);
		sb.setTexture(Skybox.Face.East, sky);
		sb.setTexture(Skybox.Face.Up, sky);
		sb.setTexture(Skybox.Face.Down, sky);
		sb.preloadTextures();
		// sb.setRenderState(blendState);
		sb.setRenderState(ts);
		sb.updateRenderState();

		rootNode.attachChild(sb);

	}

	/**
	 * Just instantiates our Car and attaches it to the scene graph.
	 */
	private void createCar() {

		String propertyName = generalProperties.getProperty("car");
		Properties carProperties = new Properties();
		try {
			carProperties.load(new FileInputStream(new File(Utils
					.getResourceURL(propertyName).toURI())));
		} catch (IOException e) {
			throw new IllegalStateException("Could not load car properties", e);
		} catch (URISyntaxException e) {
			throw new IllegalStateException("Could not load car properties", e);
		}

		CarData.setCarData(carProperties);

		car = new Car(getPhysicsSpace());
		rootNode.attachChild(car);

		setCarInitialPosition();

	}

	private void setCarInitialPosition() {
		Vector3f carPos = Utils.getPosition(gameModeProperties
				.getProperty("car.initialPosition"));

		car.setPosition(carPos.x, carPos.y, carPos.z);
	}

	/**
	 * Creates a jME ChaseCamera for the car chassis and adds custom input
	 * actions to control it.
	 */
	private void initInput(Pioneer audioSystem) {
		// Handle chase camera with mouse
		input.removeFromAttachedHandlers(cameraInputHandler);
		cameraInputHandler = chaser;
		input.addToAttachedHandlers(cameraInputHandler);

		// Attaching the custom input actions (and its respective keys) to the
		// carInput handler.
		accelActionUp = new AccelAction(audioSystem, car, 1);
		input.addAction(accelActionUp, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_UP, InputHandler.AXIS_NONE, false);
		accelActionDown = new AccelAction(audioSystem, car, -1);
		input.addAction(accelActionDown, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_DOWN, InputHandler.AXIS_NONE, false);
		steerActionUp = new SteerAction(car, -1);
		input.addAction(steerActionUp, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_LEFT, InputHandler.AXIS_NONE, false);
		steerActionDown = new SteerAction(car, 1);
		input.addAction(steerActionDown, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_RIGHT, InputHandler.AXIS_NONE, false);

		// reset action
		resetAction = new ResetAction(audioSystem, car, circuit);
		input.addAction(resetAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_S, InputHandler.AXIS_NONE, false);

		// vuvuzela-honk action
		vuvuzelaAction = new HonkAction(audioSystem);
		input.addAction(vuvuzelaAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_H, InputHandler.AXIS_NONE, false);

		// menu action
		menuAction = new MenuAction(playState);
		input.addAction(menuAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_M, InputHandler.AXIS_NONE, false);


		selectCameraAction = new SelectCameraAction(chaser);
		input.addAction(selectCameraAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_1, InputHandler.AXIS_NONE, false);
		input.addAction(selectCameraAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_2, InputHandler.AXIS_NONE, false);
		input.addAction(selectCameraAction, InputHandler.DEVICE_KEYBOARD,
				KeyInput.KEY_3, InputHandler.AXIS_NONE, false);

//		dropFlagAction = new DropFlagAction(car);
//		input.addAction(dropFlagAction, InputHandler.DEVICE_KEYBOARD,
//				KeyInput.KEY_SPACE, InputHandler.AXIS_NONE, false);
//		resetGameAction = new ResetGameAction(this);
//		input.addAction(resetGameAction, InputHandler.DEVICE_KEYBOARD,
//				KeyInput.KEY_Q, InputHandler.AXIS_NONE, false);
//		input.addAction(resetGameAction, InputHandler.DEVICE_KEYBOARD,
//				KeyInput.KEY_W, InputHandler.AXIS_NONE, false);
	}

	private void readInput() {

	}

	private void tunePhysics() {
		getPhysicsSpace().setAutoRestThreshold(0.2f);
		// Otherwise it would be VERY slow.
		setPhysicsSpeed(DEFAULT_PHYSICS_SPEED);
	}

	private void createLights() {
		DirectionalLight dl = new DirectionalLight();
		dl.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
		dl.setDirection(new Vector3f(1, -0.5f, 1));
		dl.setEnabled(true);
		lightState.attach(dl);

		DirectionalLight dr = new DirectionalLight();
		dr.setEnabled(true);
		dr.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
		dr.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
		dr.setDirection(new Vector3f(0.5f, -0.5f, 0));

		lightState.attach(dr);

	}

	private void createCircuit() {

		resetGameSettings();

		List<Observer> obs = new ArrayList<Observer>();
		obs.add(this);

		this.circuit = new Circuit(getPhysicsSpace(), gameMode,
				generalProperties, gameModeProperties, obs, lightState);

		rootNode.attachChild(this.circuit);

		rootNode.setRenderState(this.circuit.getLightState());

	}

	private void resetGameSettings() {
		currentTime = 0L;
		gameInProgress = false;
		currentScore = 0L;

		maxScore = Long.valueOf(gameModeProperties.getProperty("maxScore"));
		zeroTime = Long
				.valueOf(gameModeProperties.getProperty("zeroScoreTime"));
		lapAmount = Integer.valueOf(gameModeProperties.getProperty("laps"));

		scores = Scores.load(circuitName, gameMode);
		highScore = scores.getHighScore();

		hud.setCurrentLap(String.valueOf(0));
	}

	private void createGameStates() {

		menuState = new MenuState(rootNode, input, this, hud);
		playState = new PlayState(rootNode, input, this, hud);
		endingState = new EndGameState(rootNode, input, this, hud);

		setCurrentState(menuState);
		currentState.enterState();
	}

	public void setCurrentState(MyGameState state) {

		currentState = state;
	}

	public void setActive(boolean active) {

		this.active = active;

	}

	public void render() {

		System.out.println("setting input enabled= " + this.active);
		input.setEnabled(this.active);
	}

	public boolean isActive() {

		return this.active;

	}

	public void pauseGame() {

		setPhysicsSpeed(0);
		gamePause = new Date().getTime();
		if (audio != null)
			audio.pauseAllSounds();
	}

	public void unpauseGame() {

		gameStart += (new Date().getTime() - gamePause);
		setPhysicsSpeed(DEFAULT_PHYSICS_SPEED);
		if (audio != null)
			audio.resumeAllSounds();
	}

	// Drop a flag in-place

	public void update(Observable o, Object currentLap) {

		int lap = (Integer) currentLap;

		if (lap == 1) {
			gameStart = new Date().getTime();
			gameInProgress = true;
		}
		GameEndingState endState = gameMode.gameEnded(lap, currentTime,
				gameModeProperties);
		if (endState != GameEndingState.NOTOVER) {
			// TODO game ended
			endGame(endState);
		} else {
			hud.setCurrentLap(String.valueOf(lap));
		}

	}

	private void endGame(GameEndingState endState) {
		gameEnd = new Date().getTime();
		gameDuration = gameEnd - gameStart;
		score = gameMode.getFinalScore(gameDuration, currentScore,
				maxScore, zeroTime);

		if (score > 0L) {
			scores.addScore(new Score(score, new Date()));
			highScore = scores.save();
		}

		highScore = scores.getHighScore();
		gameInProgress = false;

		// TODO Show end game state
		hud.setGameEndType(endState);
		hud.playGameEnd(input);
	}

	public static Scores getScores() {

		return scores;
	}
}
